1 /**
2  * This module implements $(D TestSuite), an aggregator for $(D TestCase)
3  * objects to run all tests.
4  */
5 
6 module unit_threaded.testsuite;
7 
8 import unit_threaded.from;
9 
10 /*
11  * taskPool.amap only works with public functions, not closures.
12  */
13 auto runTest(from!"unit_threaded.testcase".TestCase test) {
14     return test();
15 }
16 
17 /**
18  * Responsible for running tests and printing output.
19  */
20 struct TestSuite {
21     import unit_threaded.io : Output;
22     import unit_threaded.options : Options;
23     import unit_threaded.reflection : TestData;
24     import unit_threaded.testcase : TestCase;
25     import std.datetime : Duration;
26 
27     static if (__VERSION__ >= 2077)
28         import std.datetime.stopwatch : StopWatch;
29     else
30         import std.datetime : StopWatch;
31 
32     this(in Options options, in TestData[] testData) {
33         import unit_threaded.io : WriterThread;
34 
35         this(options, testData, WriterThread.get);
36     }
37 
38     /**
39      * Params:
40      * options = The options to run tests with.
41      * testData = The information about the tests to run.
42      * output = Where to send text output.
43      */
44     this(in Options options, in TestData[] testData, Output output) {
45         import unit_threaded.factory : createTestCases;
46 
47         _options = options;
48         _testData = testData;
49         _output = output;
50         _testCases = createTestCases(testData, options.testsToRun);
51     }
52 
53     /**
54      * Runs all test cases.
55      * Returns: true if no test failed, false otherwise.
56      */
57     bool run() {
58 
59         import unit_threaded.io : writelnRed, writeln, writeRed, write,
60             writeYellow, writelnGreen;
61         import std.algorithm : filter, count;
62         import std.conv : text;
63 
64         if (!_testCases.length) {
65             _output.writelnRed("Error! No tests to run for args: ");
66             _output.writeln(_options.testsToRun);
67             return false;
68         }
69 
70         immutable elapsed = doRun();
71 
72         if (!numTestsRun) {
73             _output.writeln("Did not run any tests!!!");
74             return false;
75         }
76 
77         _output.writeln("\nTime taken: ", elapsed);
78         _output.write(numTestsRun, " test(s) run, ");
79         const failuresStr = text(_failures.length, " failed");
80         if (_failures.length) {
81             _output.writeRed(failuresStr);
82         } else {
83             _output.write(failuresStr);
84         }
85 
86         ulong numTestsWithAttr(string attr)() {
87             return _testData.filter!(a => mixin("a. " ~ attr)).count;
88         }
89 
90         void printHidden() {
91             const num = numTestsWithAttr!"hidden";
92             if (!num)
93                 return;
94             _output.write(", ");
95             _output.writeYellow(num, " ", "hidden");
96         }
97 
98         void printShouldFail() {
99             const total = numTestsWithAttr!"shouldFail";
100             ulong num = total;
101 
102             foreach (f; _failures) {
103                 const data = _testData.filter!(a => a.getPath == f).front;
104                 if (data.shouldFail)
105                     --num;
106             }
107 
108             if (!total)
109                 return;
110             _output.write(", ");
111             _output.writeYellow(num, "/", total, " ", "failing as expected");
112         }
113 
114         printHidden();
115         printShouldFail();
116 
117         _output.writeln(".\n");
118 
119         if (_options.random)
120             _output.writeln("Tests were run in random order. To repeat this run, use --seed ",
121                     _options.seed, "\n");
122 
123         if (_failures.length) {
124             _output.writelnRed("Tests failed!\n");
125             return false; //oops
126         }
127 
128         _output.writelnGreen("OK!\n");
129 
130         return true;
131     }
132 
133 private:
134 
135     const(Options) _options;
136     const(TestData)[] _testData;
137     TestCase[] _testCases;
138     string[] _failures;
139     StopWatch _stopWatch;
140     Output _output;
141 
142     /**
143      * Runs the tests with the given options.
144      * Returns: how long it took to run.
145      */
146     Duration doRun() {
147 
148         import std.algorithm : reduce;
149         import std.parallelism : taskPool;
150 
151         auto tests = getTests();
152 
153         if (_options.showChrono)
154             foreach (test; tests)
155                 test.showChrono;
156 
157         _stopWatch.start();
158 
159         if (_options.multiThreaded) {
160             _failures = reduce!((a, b) => a ~ b)(_failures, taskPool.amap!runTest(tests));
161         } else {
162             foreach (test; tests) {
163                 _failures ~= test();
164             }
165         }
166 
167         handleFailures();
168 
169         _stopWatch.stop();
170         return cast(Duration) _stopWatch.peek();
171     }
172 
173     auto getTests() {
174         import unit_threaded.io : writeln;
175 
176         auto tests = _testCases.dup;
177         if (_options.random) {
178             import std.random;
179 
180             auto generator = Random(_options.seed);
181             tests.randomShuffle(generator);
182             _output.writeln("Running tests in random order. ",
183                     "To repeat this run, use --seed ", _options.seed);
184         }
185         return tests;
186     }
187 
188     void handleFailures() {
189         import unit_threaded.io : writeln, writeRed, write;
190         import std.array : empty;
191         import std.algorithm : canFind;
192 
193         if (!_failures.empty)
194             _output.writeln("");
195         foreach (failure; _failures) {
196             _output.write("Test ", (failure.canFind(" ") ? `'` ~ failure ~ `'` : failure), " ");
197             _output.writeRed("failed");
198             _output.writeln(".");
199         }
200         if (!_failures.empty)
201             _output.writeln("");
202     }
203 
204     @property ulong numTestsRun() @trusted const {
205         import std.algorithm : map, reduce;
206 
207         return _testCases.map!(a => a.numTestsRun).reduce!((a, b) => a + b);
208     }
209 }
210 
211 /**
212  * Replace the D runtime's normal unittest block tester. If this is not done,
213  * the tests will run twice.
214  */
215 void replaceModuleUnitTester() {
216     import core.runtime : Runtime;
217 
218     Runtime.moduleUnitTester = &moduleUnitTester;
219 }
220 
221 version (unitThreadedLight) {
222 
223     shared static this() {
224         import std.algorithm : canFind;
225         import std.parallelism : parallel;
226         import core.runtime : Runtime;
227 
228         Runtime.moduleUnitTester = () {
229 
230             // ModuleInfo has opApply, can't use parallel on that so we collect
231             // all the modules with unit tests first
232             ModuleInfo*[] modules;
233             foreach (module_; ModuleInfo) {
234                 if (module_ && module_.unitTest)
235                     modules ~= module_;
236             }
237 
238             version (unitUnthreaded)
239                 enum singleThreaded = true;
240             else
241                 const singleThreaded = Runtime.args.canFind("-s")
242                     || Runtime.args.canFind("--single");
243 
244             if (singleThreaded)
245                 foreach (module_; modules)
246                     module_.unitTest()();
247             else
248                 foreach (module_; modules.parallel)
249                     module_.unitTest()();
250 
251             return true;
252         };
253     }
254 
255 } else {
256     shared static this() {
257         replaceModuleUnitTester;
258     }
259 }
260 
261 /**
262  * Replacement for the usual unittest runner. Since unit_threaded
263  * runs the tests itself, the moduleUnitTester doesn't really have to do anything.
264  */
265 private bool moduleUnitTester() {
266     //this is so unit-threaded's own tests run
267     import std.algorithm : startsWith;
268 
269     foreach (module_; ModuleInfo) {
270         if (module_ && module_.unitTest && module_.name.startsWith("unit_threaded")
271                 && // we want to run the "normal" unit tests
272                 //!module_.name.startsWith("unit_threaded.property") && // left here for fast iteration when developing
273                 !module_.name.startsWith("unit_threaded.ut.modules")) { //but not the ones from the test modules
274             version (testing_unit_threaded) {
275                 import std.stdio : writeln;
276 
277                 writeln("Running unit-threaded UT for module " ~ module_.name);
278             }
279             module_.unitTest()();
280 
281         }
282     }
283 
284     return true;
285 }